/*
 * Copyright (C) 2007-2008  Hylke van der Schaaf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, in version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package hextd.server;

import hextd.core.MapInfo;
import hextd.core.MapLoader;
import hextd.message.Message;
import hextd.message.MessageChannelCreate;
import hextd.message.MessageFactory;
import hextd.message.MessagePlayerInChannel;
import hextd.message.MessageChannelJoin;
import hextd.message.MessageFetch;
import hextd.message.MessageKickPlayer;
import hextd.message.MessageLoadMap;
import hextd.message.MessagePlayerList;
import hextd.message.MessagePong;
import hextd.message.MessageReplayLogMP;
import hextd.message.MessageReplayLogSP;
import hextd.message.MessageSay;
import hextd.message.MessageSettings;
import hextd.message.MessageSettings.settingKey;
import hextd.message.MessageTick;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;

public class Channel {

	private String name;
	private Integer chanelId;
	private Server server;
	private ArrayList<Player> players;
	private HashMap<Integer, Player> playersByUserId;
	private HashMap<Integer, Integer> userIdByGameId = new HashMap<Integer, Integer>();
	private HashMap<Integer, Integer> gameIdByUserId = new HashMap<Integer, Integer>();
	private HashMap<Integer, AtomicInteger> lastTickByUserId = new HashMap<Integer, AtomicInteger>();
	private HashMap<MessageSettings.settingKey, String> settings = new HashMap<MessageSettings.settingKey, String>();
	private int maxGameId = 0;
	private boolean gameStarted = false;
	private StringBuffer gameLog = new StringBuffer();
	private StringBuffer gameLogShort = new StringBuffer();
	private boolean open = true;
	private boolean logSend = false;
	private MapLoader mapLoader;
	private MapInfo selectedMap;
	private String version = "unknown";
	private String mapMD5 = "unknown";
	private String unknownSelectedMap = null;
	private Timer countdownTimer;
	private TimerTask countdownTimerTask;
	private int mapStartCountdown = 5;
	private String mapStartMessage;

	public Channel(Server server, String name, Integer id, Timer countdownTimer) {
		this.server = server;
		this.name = name;
		this.chanelId = id;
		this.countdownTimer = countdownTimer;
		this.players = new ArrayList<Player>();
		this.playersByUserId = new HashMap<Integer, Player>();
		mapLoader = server.getMapLoader();
		settings.put(settingKey.pause, Boolean.toString(false));
		settings.put(settingKey.ladderGame, Boolean.toString(false));
		settings.put(settingKey.publicGame, Boolean.toString(true));
		settings.put(settingKey.openGame, Boolean.toString(true));
	}

	public synchronized void playerJoined(Player player) {
		if (open) {
			System.out.println("Channel::playerJoined: " + player.getUserName() + "->" + this.name);
			sendLine(MessageSay.formatMessage(-1, "<div class='playerJoined'> &gt;&gt;&gt; " + player.getUserName() + " joined.</div>"));

			AtomicInteger lastTick = lastTickByUserId.get(player.getUserId());
			if (lastTick == null) {
				lastTick = new AtomicInteger(0);
				lastTickByUserId.put(player.getUserId(), lastTick);
			}

			if (gameStarted) {
				int userId = player.getUserId();
				int gameId;
				if (gameIdByUserId.containsKey(userId)) {
					gameId = gameIdByUserId.get(userId);
					players.add(Math.min(gameId, players.size()), player);
					playersByUserId.put(player.getUserId(), player);
					player.setChannel(this);
					player.setGameId(gameId);
				} else {
					gameId = ++maxGameId;
					player.setGameId(gameId);
					players.add(player);
					playersByUserId.put(player.getUserId(), player);
					player.setChannel(this);
					userIdByGameId.put(gameId, userId);
					gameIdByUserId.put(userId, gameId);
				}
				if (settings.size() > 0) {
					player.sendLine(MessageSettings.formatMessage(-1, settings));
				}
				player.sendLine(MessagePlayerInChannel.formatMessage(this.chanelId, gameId, gameStarted, lastTick.get()));
				player.sendLine(MessageSay.formatMessage(-1, "<div class='channelJoin'>Hello " + player.getUserName() + ", Welcome to " + this.name + "</div>"));
				player.sendLine(this.gameLog.toString());
			} else {
				players.add(player);
				playersByUserId.put(player.getUserId(), player);
				player.setChannel(this);
				int userId = player.getUserId();
				int gameId = players.indexOf(player);
				maxGameId = players.size() - 1;
				userIdByGameId.put(gameId, userId);
				gameIdByUserId.put(userId, gameId);
				player.setGameId(gameId);
				player.sendLine(MessagePlayerInChannel.formatMessage(this.chanelId, gameId, gameStarted, lastTick.get()));
				player.sendLine(MessageSay.formatMessage(-1, "<div class='channelJoin'>Hello " + player.getUserName() + ", Welcome to " + this.name + "</div>"));
				if (settings.size() > 0) {
					player.sendLine(MessageSettings.formatMessage(-1, settings));
				}
			}
			this.sendPlayerList();
		}
	}

	public synchronized void playerLeft(Player player, String to) {
		int playerId = players.indexOf(player);
		players.remove(player);
		playersByUserId.remove(player.getUserId());
		if (players.size() > 0) {
			if (!gameStarted) {
				gameIdByUserId.remove(player.getUserId());
				if (playerId >= 0) {
					for (int i = playerId; i < players.size(); i++) {
						Player renumberPlayer = players.get(i);
						int userId = renumberPlayer.getUserId();
						userIdByGameId.remove(i + 1);
						renumberPlayer.setGameId(i);
						userIdByGameId.put(i, userId);
						gameIdByUserId.put(userId, i);
						renumberPlayer.sendLine(MessagePlayerInChannel.formatMessage(chanelId, i, gameStarted, 0));
					}

					maxGameId--;
				} else {
					System.out.println("Channel::playerLeft: playerId was < 0: " + playerId);
				}

			}
			sendPlayerList();
			if (to != null) {
				sendLine(MessageSay.formatMessage(-1, "<div class='playerJoined'> &lt;&lt;&lt; " + player.getUserName() + " left to " + to + ".</div>"));
			}
		} else {
			server.channelEmpty(this);
		}

	}

	public synchronized void receiveLine(Player from, String line) {
		boolean log = true;
		Message message = MessageFactory.GetMessage(line);
		switch (message.getType()) {
			case Ping:
				from.sendLine(MessagePong.formatMessage());
				return;

			case Tick:
				log = false;
				MessageTick mt = (MessageTick) message;
				lastTickByUserId.get(from.getUserId()).set(mt.getTick());
			case BuyCreep:
			case TowerBuy:
			case TowerUpgrade:
			case TowerSell:
			case SetMode:
			case GameEnd:
			case PlayerForfeit:
				if (message.getSenderId() != from.getGameId()) {
					System.out.println("Channel::reveiveLine: PlayerForfeit senderID not correct, got: " + message.getSenderId() + " expected " + from.getGameId());
					message.setSenderId(from.getGameId());
				}
				break;

			case Settings:
				if (message.getSenderId() != from.getUserId()) {
					message.setSenderId(from.getUserId());
				}

				MessageSettings ms = (MessageSettings) message;
				if (from.getGameId() == 0 && !gameStarted) {
					settings.putAll(ms.getSettings());
					for (Entry<settingKey, String> entrySet : ms.getSettings().entrySet()) {
						switch (entrySet.getKey()) {
							case openGame:
								this.open = Boolean.valueOf(entrySet.getValue());
								break;
							case selectedMap:
								try {
									int mapId = Integer.parseInt(entrySet.getValue());
									selectedMap = mapLoader.getMap(mapId);
									unknownSelectedMap = null;
								} catch (NumberFormatException e) {
									unknownSelectedMap = entrySet.getValue();
									System.out.println("Channel::receivedLine: Unknown map selected: " + entrySet.getValue());
								}
								break;
							case mapMD5:
								mapMD5 = entrySet.getValue();
								break;
							case version:
								version = entrySet.getValue();
								break;
						}
					}
				}
				for (Entry<settingKey, String> entrySet : ms.getSettings().entrySet()) {
					switch (entrySet.getKey()) {
						case pause:
							settings.put(entrySet.getKey(), entrySet.getValue());
							break;
						case ready:
							if (!gameStarted) {
								from.setReady(Boolean.parseBoolean(entrySet.getValue()));
								if (checkReady()) {
									if (unknownSelectedMap != null) {
										mapStartMessage = MessageLoadMap.formatMessage(unknownSelectedMap, mapMD5, version);
									} else {
										mapStartMessage = MessageLoadMap.formatMessage("" + selectedMap.mapId, mapMD5, version);
									}
									startMapStartCountdown();
								} else {
									stopMapStartCountdown();
								}
							}
							break;
					}
				}

				break;

			case Say:
				if (message.getSenderId() != from.getUserId()) {
					System.out.println("Channel::reveiveLine: Say senderID not correct, got: " + message.getSenderId() + " expected " + from.getUserId());
					message.setSenderId(from.getUserId());
				}
				break;

			case ChannelCreate:
				MessageChannelCreate mca = (MessageChannelCreate) message;
				server.createChannel(mca.getName(), from);
				return; // don't log/forward this message

			case ChannelJoin:
				MessageChannelJoin mjc = (MessageChannelJoin) message;
				server.playerToChannel(from, mjc.getId());
				return; // don't log/forward this message

			case KickPlayer:
				if (from.getGameId() == 0) {
					MessageKickPlayer mkp = (MessageKickPlayer) message;
					Player target = playersByUserId.get(mkp.getTarget());
					if (target != null) {
						server.playerToChannel(target, 0);
					}
				} else {
					System.out.println("Channel::reveiveLine: KickPlayer sender not 0 but " + from.getGameId());
				}
				return;

			case Fetch:
				MessageFetch mf = (MessageFetch) message;
				server.fetchFromServer(mf, from);
				return; // don't log/forward this message

			case LoadMap:
				// This message should never originate on a client and should thus never be received by the channel.
				System.out.println("Channel::lineReceived: ???? MessageLoadMap reveived ???? " + this.name + "");
				return;

			case ReplayLogSP:
				MessageReplayLogSP mrl = (MessageReplayLogSP) message;
				mrl.setUserId(from.getUserId());
				server.addSpReplay(mrl);
				return;

			case Unknown:
				if (line.length() > 0) {
					System.out.println("Channel::lineReceived: Unknown message: " + line);
				}
				return;

			default:
				System.err.println("Channel::lineReceived: Unknown message type: " + message.getType());
				return;
		}

		String forward = message.getMessage();
		if (log) {
			gameLogShort.append(forward + "\n");
		}

		gameLog.append(forward + "\n");
		for (Player p : players) {
			if (p != from) {
				p.sendLine(forward);
			}

		}
		switch (message.getType()) {
			case GameEnd:
				open = false;
				boolean doSend = false;
				synchronized (this) {
					if (!logSend) {
						logSend = true;
						doSend = logSend;
					}
				}
				if (doSend) {
					MessageReplayLogMP replay = new MessageReplayLogMP();

					replay.setLog(gameLogShort.toString());
					replay.setPublic(Boolean.parseBoolean(settings.get(settingKey.publicGame)));
					replay.setLadder(Boolean.parseBoolean(settings.get(settingKey.ladderGame)));
					server.addMpReplay(replay);
				}

				break;
		}

	}

	private boolean checkReady() {
		if (unknownSelectedMap != null) {
			for (int i = 0; i < players.size(); i++) {
				Player p = players.get(i);
				if (!p.isReady()) {
					return false;
				}
			}
		} else {
			if (selectedMap == null) {
				return false;
			}
			int numPlayers = selectedMap.players;
			if (players.size() < numPlayers) {
				return false;
			}
			for (int i = 0; i < numPlayers; i++) {
				Player p = players.get(i);
				if (!p.isReady()) {
					return false;
				}
			}
		}
		return true;
	}

	private void stopMapStartCountdown() {
		if (countdownTimerTask != null) {
			countdownTimerTask.cancel();
			countdownTimerTask = null;
			sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game start cancelled.</div>"));
		}
	}

	private void startMapStartCountdown() {
		if (countdownTimerTask == null) {
			mapStartCountdown = 5;
			countdownTimerTask = new TimerTask() {

				@Override
				public void run() {
					mapStartCountdownTick();
				}
			};
			countdownTimer.scheduleAtFixedRate(countdownTimerTask, 100, 1000);
		}
	}

	private void mapStartCountdownTick() {
		if (mapStartCountdown <= 0) {
			sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game starts NOW!</div>"));
			countdownTimerTask.cancel();
			countdownTimerTask = null;

			gameLog.delete(0, gameLog.length());
			gameLogShort.delete(0, gameLogShort.length());
			gameStarted = true;
			logSend = false;

			sendLine(mapStartMessage);
			gameLog.append(mapStartMessage + "\n");
			gameLogShort.append(mapStartMessage + "\n");
			gameLog.append(MessagePlayerList.formatMessage(players) + "\n");
			gameLogShort.append(MessagePlayerList.formatMessage(players) + "\n");

			mapStartMessage = null;
		} else if (mapStartCountdown == 2) {
			sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game start in " + mapStartCountdown + "...</div>"));
			gameStarted = true;
			mapStartCountdown--;
		} else {
			sendLine(MessageSay.formatMessage(-1, "<div class='channel'> &gt;&gt;&gt; Game start in " + mapStartCountdown + "...</div>"));
			mapStartCountdown--;
		}
	}

	public void sendPlayerList() {
		String message = MessagePlayerList.formatMessage(players);
		this.sendLine(message);
	}

	public void sendLine(String line) {
		for (Player p : this.players) {
			p.sendLine(line);
		}

	}

	public Integer getId() {
		return chanelId;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	@Override
	public String toString() {
		return name;
	}

	public boolean isOpen() {
		return open;
	}

	public boolean isGameStarted() {
		return gameStarted;
	}

	public int getPlayerCount() {
		return players.size();
	}
}
